#!usr/bin/env python3
#
# Wind Minecraft Downloader
# LICENSED UNDER MIT LICENSE.
#

import os, sys, json, shutil, logging, threading, time
from windlib import get_file, get_sha1, pushd, typeof
from pathlib import Path

# ========= VERSION INFO =========#

PROGRAM_VERSION = 'Alpha 2.0.0'

# ========= VERSION INFO =========#


# 日志设置
logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')

logging.info('Main: Starting WMCD ' + PROGRAM_VERSION)


# 预调用函数，该函数已不存在于 windlib，故在此补写。
def file_or_dir_exists(target: str) -> str:
    """
    检查指定的文件(或文件夹)是否存在。

    :param target: 目标路径

    :return: 当目标是目录时，会返回'IS_DIR'，当目标是文件时，会返回'IS_FILE'，当函数找不到目标时，会返回'NOT_FOUND'，当目标不是有效路径是，会返回'TARGET_INVAILD'。
    """
    try:
        f = Path(target)
    except:
        return 'TARGET_INVAILD'
    if f.is_file():
        return 'IS_FILE'
    elif f.is_dir():
        return 'IS_DIR'
    else:
        return 'NOT_FOUND'


currdir = os.getcwd()

del sys.argv[0]

arg_dict = {}

for tmp in sys.argv:
    argv = tmp[2:]
    argv = argv.split('=')
    arg_dict[argv[0]] = argv[1]

if sys.platform == 'win32':
    platform = 'windows'
elif sys.platform == 'darwin':
    platform = 'osx'
elif sys.platform == 'linux':
    platform = 'linux'
else:
    logging.warning('Main: Unknown operating system! The natives libraries will cannot download.')
    platform = None

# 命令行参数处理

mcroot = './minecraft'

try:
    dlfrom = arg_dict['dlfrom']
except:
    dlfrom = 'BMCLAPI'

mcroot = os.path.abspath(arg_dict['mcroot'])

if not file_or_dir_exists(mcroot):
    os.mkdir(mcroot)

if not dlfrom in ('BMCLAPI', 'MCBBS', 'MOJANG'):
    logging.warning('Main: the \'dlfrom\' argument is invaild. We will use the default value \'BMCLAPI\'.')
    dlfrom = 'BMCLAPI'

# 临时目录
if not file_or_dir_exists('./tmp'):
    os.mkdir('tmp')


def replace_mojang_websites(AnyStr):
    '''
    此函数用于将 Mojang 官方下载源 URL 替换成 BMCLAPI 或 MCBBS 下载源 URL。

    :param AnyStr: 指定的 Mojang 官方下载源 URL。

    :return: 经过替换的 BMCLAPI 或 MCBBS 下载源 URL。
    '''
    if dlfrom == 'BMCLAPI':
        AnyStr = AnyStr.replace('https://launchermeta.mojang.com', 'https://bmclapi2.bangbang93.com').replace(
            'https://launcher.mojang.com', 'https://bmclapi2.bangbang93.com').replace('https://libraries.minecraft.net',
                                                                                      'https://bmclapi2.bangbang93.com/maven')
    elif dlfrom == 'MCBBS':
        AnyStr = AnyStr.replace('https://launchermeta.mojang.com', 'https://download.mcbbs.net').replace(
            'https://launcher.mojang.com', 'https://download.mcbbs.net').replace('https://libraries.minecraft.net',
                                                                                 'https://download.mcbbs.net/maven')
    return AnyStr


# 获取版本信息
logging.info('Main: Loading version manifest.')
if dlfrom == 'BMCLAPI':
    version_manifest_ol = 'https://bmclapi2.bangbang93.com/mc/game/version_manifest.json'
    asset_domain = 'https://bmclapi2.bangbang93.com/assets/'
elif dlfrom == 'MCBBS':
    version_manifest_ol = 'https://download.mcbbs.net/mc/game/version_manifest.json'
    asset_domain = 'https://download.mcbbs.net/assets/'
else:
    version_manifest_ol = 'https://launchermeta.mojang.com/mc/game/version_manifest.json'
    asset_domain = 'https://resources.download.minecraft.net/'
Retry = 0

while True:
    with pushd('./tmp'):
        logging.info('Main: Downloading version manifest.')
        version_manifest_local = get_file(version_manifest_ol)
        if version_manifest_local == 'DOWNLOAD_FAILED':
            if Retry > 5:
                logging.error('Main: Failed to download version manifest. Program cannot continue.')
                sys.exit()
            logging.warning('Main: Failed to download version manifest. We will retry after 3 seconds...')
            Retry += 1
            time.sleep(3)
            continue
        with open(version_manifest_local, 'r', encoding='utf-8') as f:
            version_manifest = json.loads(replace_mojang_websites(f.readline()))
            break

del version_manifest_local, version_manifest_ol, Retry

releases = snapshots = old_beta = old_alpha = []

for tmp in version_manifest['versions']:
    if tmp['type'] == 'release':
        releases.append(tmp)
    elif tmp['type'] == 'snapshot':
        snapshots.append(tmp)
    elif tmp['type'] == 'old_beta':
        old_beta.append(tmp)
    elif tmp['type'] == 'old_alpha':
        old_alpha.append(tmp)

# 移除已存在的临时目录 (如果有)

try:
    shutil.rmtree('./tmp')
except:
    pass

# 处理 Minecraft 文件夹

assets_dir = {'indexes': os.path.abspath(mcroot + '/assets/indexes'),
              'objects': os.path.abspath(mcroot + '/assets/objects')}
libraries_dir = os.path.abspath(mcroot + '/libraries')
versions_dir = os.path.abspath(mcroot + '/versions/')

os.chdir(mcroot)
if not file_or_dir_exists('assets'):
    os.mkdir('assets')
with pushd('./assets'):
    if not file_or_dir_exists('./indexes'):
        os.mkdir('./indexes')
    if not file_or_dir_exists('./objects'):
        os.mkdir('./objects')
if not file_or_dir_exists('./libraries'):
    os.mkdir('./libraries')
if not file_or_dir_exists('./versions'):
    os.mkdir('./versions')


# 函数

def downloadAssets(ver_json):
    '''
    此函数用于下载指定版本的 Assets 资源文件，下载源默认为 BMCLAPI。

    :param ver_json: 指定的版本 JSON 文件。

    :return:此函数不返回值。
    '''
    os.chdir(currdir)
    # 获取 资源文件索引 并下载
    assetIndexURL = replace_mojang_websites(ver_json['assetIndex']['url'])
    Retry = Retry_ = 0
    while True:
        assetIndexLocal = get_file(assetIndexURL, save_path=assets_dir['indexes'])
        if assetIndexLocal == 'DOWNLOAD_FAILED':
            if Retry > 5:
                logging.error('downloadAssets: Failed to download asset index json file. We will not try again.')
                sys.exit(1)
            logging.warning('downloadAssets: Failed to download asset index json file. We will retry after 3 seconds.')
            Retry += 1
            time.sleep(3)
        if not get_sha1(assetIndexLocal) == ver_json['assetIndex']['sha1']:
            if Retry_ > 5:
                logging.error(
                    'downloadAssets: The asset index json file in your disk maybe broken. We will not try again.')
                sys.exit(1)
            logging.warning(
                'downloadAssets: The asset index json file in your disk maybe broken. We will delete it and download it again after 3 seconds.')
            Retry_ += 1
            time.sleep(3)
            continue
        break
    del assetIndexURL, Retry
    # 解析 索引
    with open(assetIndexLocal, 'r', encoding='utf-8') as f:
        assetIndexJSON = json.loads(f.readline())
    for tmp in assetIndexJSON['objects']:
        os.chdir(assets_dir['objects'])
        hashid = assetIndexJSON['objects'][tmp]['hash']
        folder = hashid[0:2]
        if not file_or_dir_exists(folder):
            os.mkdir(folder)
        os.chdir(folder)
        resource_ol = asset_domain + folder + '/' + hashid
        logging.info('downloadAssets: downloading resource file ' + hashid)
        Retry = 0
        while True:
            temp = get_file(resource_ol)
            if temp == 'DOWNLOAD_FAILED':
                if Retry > 5:
                    logging.error(
                        'downloadAssets: Failed to download resource file ' + hashid + ' . We will not try again.')
                    break
                logging.warning(
                    'downloadAssets: Failed to download resource file ' + hashid + ' . We will retry after 3 seconds.')
                Retry += 1
                time.sleep(3)
            break
    logging.info('downloadAssets: Thread end.')


def downloadLibraries(ver_json):
    '''
    此函数用于下载指定版本的 libraries 库文件，下载源默认为 BMCLAPI。

    :param ver_json: 指定的版本 JSON 文件。

    :return:此函数不返回值。
    '''
    os.chdir(libraries_dir)
    libraries_dict = ver_json['libraries']
    del ver_json
    for tmp in libraries_dict:
        os.chdir(libraries_dir)
        logging.info('downloadLibraries: downloading ' + tmp['name'])
        # 处理规则
        NoDL = False
        try:
            rules = tmp['rules']
            for temp in rules:
                action = rules['action']
                try:
                    requried = rules['os']['name']
                except:
                    requried = None
                if action == 'disallow':
                    if typeof(requried) == 'list':
                        if platform in requried:
                            NoDL = True
                            break
                    else:
                        if platform == requried:
                            NoDL = True
                            break
        except:
            pass
        if NoDL == True:
            continue
        # 处理natives (如果有)
        try:
            natives = tmp['natives']
            if platform == 'windows':
                natives_info = natives['natives-windows']
            elif platform == 'macos':
                natives_info = natives['natives-macos']
            elif platform == 'osx':
                natives_info = natives['natives-osx']
            elif platform == 'linux':
                natives_info = natives['natives-linux']
            del natives
            print(natives_info)
        except:
            pass
        # 处理路径
        temp = tmp['downloads']['artifact']['path'].split('/')
        for temp_ in temp:
            if not file_or_dir_exists(temp_):
                os.mkdir(temp_)
            os.chdir(temp_)
        # 下载文件
        URL = tmp['downloads']['artifact']['url']
        temp_ = os.path.basename(URL).replace('.jar', '').split('-')[1]
        if not file_or_dir_exists(temp_):
            os.mkdir(temp_)
        os.chdir(temp_)
        del temp, temp_
        Retry = Retry_ = 0
        while True:
            test = get_file(URL)
            if test == 'DOWNLOAD_FAILED':
                if Retry > 5:
                    logging.error('downloadLibraries: Failed to download ' + tmp['name'] + ' . We will not try again.')
                    break
                logging.warning(
                    'downloadLibraries: Failed to download ' + tmp['name'] + ' . We will retry after 3 seconds.')
                time.sleep(3)
                # 检查文件是否损坏
            if not get_sha1(test) == tmp['downloads']['artifact']['sha1']:
                if Retry_ > 5:
                    logging.error(
                        'downloadLibraries: The java archive in your disk maybe broken. We will not try again.')
                    break
                logging.warning(
                    'downloadLibraries: The java archive in your disk maybe broken. We will download it again after 3 seconds.')
                time.sleep(3)
            break
    logging.info('downloadLibraries: Thread end.')


class dlasset(threading.Thread):
    def __init__(self, ver_json):
        threading.Thread.__init__(self)
        self.ver_json = ver_json

    def run(self):
        downloadAssets(self.ver_json)


class dllib(threading.Thread):
    def __init__(self, ver_json):
        threading.Thread.__init__(self)
        self.ver_json = ver_json

    def run(self):
        downloadLibraries(self.ver_json)


def download(ver_name):
    '''
    下载一个完整的 Minecraft 版本。

    :param ver_name: 指定的版本号。

    :return: 此函数不返回值。
    '''
    os.chdir(versions_dir)  # 切换到总版本文件夹，防止出错
    ver_json_online = None
    for tmp in version_manifest['versions']:
        if tmp['id'] == ver_name:
            # 获取 版本信息JSON 的下载地址
            ver_json_online = replace_mojang_websites(tmp['url'])
            break
    if ver_json_online == None:
        return 'VERSION_NOT_FOUND'
    if not file_or_dir_exists(ver_name):
        os.mkdir(ver_name)
    os.chdir(ver_name)
    Retry = 0
    while True:
        ver_json_local = get_file(ver_json_online)
        if ver_json_local == 'DOWNLOAD_FAILED':
            if Retry > 5:
                logging.error('download: Failed to download version json. We will not try again.')
                sys.exit(1)
            logging.warning('download: Failed to download version json. We will try again after 3 seconds.')
            time.sleep(3)
            continue
        break
    del ver_json_online
    # 将版本信息JSON 转为 字典
    with open(ver_json_local, 'r') as f:
        ver_json = json.loads(replace_mojang_websites(f.readline()))
    del ver_json_local
    # 获取 客户端核心 的下载地址和SHA1
    ver_client = ver_json['downloads']['client']['url']
    ver_client_sha1 = ver_json['downloads']['client']['sha1']
    # 下载 客户端核心 并 校验
    Retry = Retry_ = 0
    while True:
        test = get_file(ver_client)
        if test == 'DOWNLOAD_FAILED':
            if Retry > 5:
                logging.error('download: Failed to download client java archive. We will not try again.')
                break
            logging.warning('download: Failed to download client java archive. We will try again after 3 seconds.')
            time.sleep(3)
            continue
        if not get_sha1(test) == ver_client_sha1:
            if Retry_ > 5:
                logging.error(
                    'download: The client java archive in your disk maybe broken. We will not download it again.')
                break
            logging.warning(
                'download: The client java archive in your disk maybe broken. We will download it again after 3 seconds.')
            time.sleep(3)
            continue
        os.rename('client.jar', ver_name + '.jar')
        break
    threads = []
    dlassetThread = dlasset(ver_json)
    dllibThread = dllib(ver_json)
    dlassetThread.start()
    dllibThread.start()
    threads.append(dlassetThread)
    threads.append(dllibThread)
    for t in threads:
        t.join()
    verify_files(ver_json)


def verify_files(ver_json_filename):
    '''
    校验指定的 Minecraft 版本，校验有误则重新下载。

    :param ver_json_filename: 指定的版本号。

    :return: 此函数不返回值。
    '''
    logging.info('verify_file: Verifying ' + os.path.basename(ver_json_filename).replace('.json', ''))
    try:
        with open(ver_json_filename, 'r') as f:
            ver_json = json.loads(replace_mojang_websites(f.readline()))
    except:
        logging.error('verify_files: The filename is invaild.')
        return 'FILENAME_INVAILD'
    # 验证 资源文件
    with open(assets_dir['indexes'] + '/' + ver_json['assets'] + '.json', 'r') as f:
        assetJSON = json.loads(f.readline())
    for tmp in assetJSON['objects']:
        hashid = assetJSON[tmp]['hash']
        folder = hashid[0:2]
        logging.info('verify_files: Verifying resource file ' + hashid)
        if assetJSON[tmp]['hash'] == get_sha1(assets_dir['objects'] + '/' + folder + '/' + hashid):
            continue
        logging.warning('verify_files: Resource file ' + hashid + ' maybe broken. We will download it.')
        resource_ol = asset_domain + folder + '/' + hashid
        os.chdir(assets_dir['objects'] + '/' + folder)
        test = get_file(resource_ol)
        if test == 'DOWNLOAD_FAILED':
            logging.warning('verify_files: Failed to download ' + hashid)
    # 验证支持库


def get_ver_manifest(target_type='ALL', output=None):
    '''
    获取 Minecraft 指定版本类型的列表，例如正式版、快照版列表等。支持把版本列表写入到一个文件，默认不写入。

    :param target_type: 目标版本类型。

    :param output: 写入的文件名。

    :return: 如果将列表写入文件，则此函数不返回值；否则，返回查询得到的列表。
    '''
    if target_type == 'release':
        will = releases
    elif target_type == 'snapshot':
        will = snapshots
    elif target_type == 'old_beta':
        will = old_beta
    elif target_type == 'old_alpha':
        will = old_alpha
    elif target_type == 'ALL':
        will = version_manifest
    else:
        will = {'return': 'INVAILD_GAMETYPE'}
    if not output == None:
        with open(output, 'w', encoding='utf-8') as f:
            f.write(json.dumps(will))
    else:
        return will


# 应用命令行参数
if __name__ == '__main__':
    try:
        download(arg_dict['download'])
    except:
        pass
    try:
        verify_files(arg_dict['verify'])
    except:
        pass
    try:
        tmp = arg_dict['get_gametype'].split(',')
        get_ver_manifest(tmp[0], tmp[1])
    except:
        pass

